using System;
using System.ComponentModel;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Xml;
using System.Xml.Schema;
using System.Xml.Linq;
using System.Linq;

using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Content.Pipeline;
using Microsoft.Xna.Framework.Content.Pipeline.Graphics;
using Microsoft.Xna.Framework.Content.Pipeline.Processors;

namespace XnaGuiContentPipeline
{
    [ContentProcessor(DisplayName = "CellarDoor UiThemeProcessor")]
    public class UiThemeProcessor : ContentProcessor<UiThemeContent, UiThemeContent>
    {
        private Color colorKeyColor;
        private bool colorKeyEnabled;
        private TextureProcessorOutputFormat textureFormat;
        private const int MAX_SIZE = 1024;

        /// <summary>Gets or sets the color value to replace with transparent black.</summary>
        /// <returns>Color value of the material to replace with transparent black.</returns>
        [DefaultValue(typeof(Color), "255, 0, 255, 255")]
        public virtual Color ColorKeyColor
        {
            get { return this.colorKeyColor; }
            set { this.colorKeyColor = value; }
        }

        /// <summary>Specifies whether color keying of a texture is enabled.</summary>
        /// <returns>true if color keying is enabled; false otherwise.</returns>
        [DefaultValue(false)]
        public virtual bool ColorKeyEnabled
        {
            get { return this.colorKeyEnabled; }
            set { this.colorKeyEnabled = value; }
        }

        /// <summary>Specifies the texture format of output materials.</summary>
        /// <returns>The texture format of the output.</returns>
        [DefaultValue(2)]
        public virtual TextureProcessorOutputFormat TextureFormat
        {
            get { return this.textureFormat; }
            set { this.textureFormat = value; }
        }

        private class PackedBitmap
        {
            public Texture2DContent Texture;
            public BitmapContent Bitmap;
            public BitmapContent Sprite;
            public Rectangle Offset;
        }

        public override UiThemeContent Process(UiThemeContent input, ContentProcessorContext context)
        {
            this.BuildAllTextures(input, context);
            this.BuildAllFonts(input, context);
            return input;
        }

        private void BuildAllTextures(UiThemeContent input, ContentProcessorContext context)
        {
            // System.Diagnostics.Debugger.Launch();

            Dictionary<string, BitmapContent> processed = new Dictionary<string, BitmapContent>();

            // iterate over all sprite entries and build and load the textures
            // cache them in the processed dictionary
            foreach (ThemeElementContent elementContent in input.Elements)
            {
                foreach (ThemeSpriteContent sprite in elementContent.States)
                {
                    string key;
                    TextureContent texture;
                    
                    if (BuildTexture(sprite.Texture, context, out key, out texture))
                    {
                        if (!processed.ContainsKey(key))
                        {
                            processed.Add(key, texture.Faces[0][0]);
                        }
                    }
                }
            }

            foreach (var pair in input.Sprites)
            {
                ThemeSpriteContent sprite = pair.Value;
                string key;
                TextureContent texture;

                if (BuildTexture(sprite.Texture, context, out key, out texture))
                {
                    if (!processed.ContainsKey(key))
                    {
                        processed.Add(key, texture.Faces[0][0]);
                    }
                }
            }

            // pack all processed textures
            Dictionary<string, PackedBitmap> packed = PackAllTextures(input, context, processed);

            // iterate over all sprite entries again. Now shift the sprite definition
            // by the offset of the packed texture
            foreach (ThemeElementContent elementContent in input.Elements)
            {
                foreach (ThemeSpriteContent sprite in elementContent.States)
                {
                    PackedBitmap pack = packed[sprite.Texture.Filename];

                    sprite.TextureContent = pack.Texture;
                    sprite.Rectangle.rectangle1.X += pack.Offset.X;
                    sprite.Rectangle.rectangle1.Y += pack.Offset.Y;
                    sprite.Rectangle.rectangle2.X += pack.Offset.X;
                    sprite.Rectangle.rectangle2.Y += pack.Offset.Y;
                }
            }
            foreach (var pair in input.Sprites)
            {
                ThemeSpriteContent sprite = pair.Value;
                PackedBitmap pack = packed[sprite.Texture.Filename];

                sprite.TextureContent = pack.Texture;
                sprite.Rectangle.rectangle1.X += pack.Offset.X;
                sprite.Rectangle.rectangle1.Y += pack.Offset.Y;
                sprite.Rectangle.rectangle2.X += pack.Offset.X;
                sprite.Rectangle.rectangle2.Y += pack.Offset.Y;
            }
        }

        private Dictionary<string, PackedBitmap> PackAllTextures(
            UiThemeContent input, ContentProcessorContext context, 
            Dictionary<string, BitmapContent> bitmaps)
        {
            // order the bitmaps by height
            var orderetBitmaps = bitmaps.Values.OrderBy<BitmapContent, int>((bitmap) => bitmap.Height);

            var result = new Dictionary<string, PackedBitmap>();

            // create the initial spritesheet
            var currentSheet = new PixelBitmapContent<Color>(MAX_SIZE, MAX_SIZE);
            var currentTexture = new Texture2DContent();
            currentTexture.Mipmaps.Add(currentSheet);

            int left = 0;
            int height = 0;
            int maxHeight = 0;

            foreach (var item in orderetBitmaps)
	        {
                string key = "";
                foreach (var pair in bitmaps)
                {
                    if (pair.Value == item)
                    {
                        key = pair.Key;
                    }
                }
                System.Diagnostics.Debug.Assert(key != "", "whoops");

                // when a sprite exceeds maximum size, it goes to its own texture
                if (item.Width + 2 >= MAX_SIZE || item.Height + 2 >= MAX_SIZE)
                {
                    PackedBitmap pack = new PackedBitmap();
                    pack.Offset = new Rectangle(0, 0, item.Width, item.Height);
                    pack.Sprite = item;
                    pack.Bitmap = item;
                    pack.Texture = currentTexture;
                    
                    result.Add(key, pack);
                    continue;
                }

                int newLeft   = (2 + left + item.Width);
                int newHeight = (2 + height + item.Height);

                // check if it fits in current row
                if (newHeight < MAX_SIZE)
                {
                    if (newLeft < MAX_SIZE)
                    {
                        CopyAndPackBitmap(item, currentSheet, left, height, result, key, currentTexture);
                        if (newHeight > maxHeight) maxHeight = newHeight;
                        left = newLeft;

                        continue;
                    }

                    left = 0;
                    height = maxHeight;

                    newHeight = (2 + height + item.Height);
                    newHeight = (2 + height + item.Height);

                    if (newHeight < MAX_SIZE)
                    {
                        CopyAndPackBitmap(item, currentSheet, left, height, result, key, currentTexture);
                        if (newHeight > maxHeight) maxHeight = newHeight;
                        left = newLeft;

                        continue;
                    }
                }
                
                currentSheet = new PixelBitmapContent<Color>(MAX_SIZE, MAX_SIZE);
                currentTexture = new Texture2DContent();
                currentTexture.Faces.Add(currentSheet);

                CopyAndPackBitmap(item, currentSheet, 0, 0, result, key, currentTexture);

                left = item.Width + 2;
                height = 0;
                maxHeight = item.Height + 2;
	        }

            return result;
        }

        private void CopyAndPackBitmap(
            BitmapContent source, BitmapContent output, int x, int y, 
            Dictionary<string, PackedBitmap> bucket, string key, Texture2DContent texture)
        {
            int w = source.Width;
            int h = source.Height;

            // Copy the main sprite data to the output sheet.
            BitmapContent.Copy(source, new Rectangle(0, 0, w, h),
                               output, new Rectangle(x + 1, y + 1, w, h));

            // Copy a border strip from each edge of the sprite, creating
            // a one pixel padding area to avoid filtering problems if the
            // sprite is scaled or rotated.
            BitmapContent.Copy(source, new Rectangle(0, 0, 1, h),
                               output, new Rectangle(x, y + 1, 1, h));

            BitmapContent.Copy(source, new Rectangle(w - 1, 0, 1, h),
                               output, new Rectangle(x + w + 1, y + 1, 1, h));

            BitmapContent.Copy(source, new Rectangle(0, 0, w, 1),
                               output, new Rectangle(x + 1, y, w, 1));

            BitmapContent.Copy(source, new Rectangle(0, h - 1, w, 1),
                               output, new Rectangle(x + 1, y + h + 1, w, 1));

            // Copy a single pixel from each corner of the sprite,
            // filling in the corners of the one pixel padding area.
            BitmapContent.Copy(source, new Rectangle(0, 0, 1, 1),
                               output, new Rectangle(x, y, 1, 1));

            BitmapContent.Copy(source, new Rectangle(w - 1, 0, 1, 1),
                               output, new Rectangle(x + w + 1, y, 1, 1));

            BitmapContent.Copy(source, new Rectangle(0, h - 1, 1, 1),
                               output, new Rectangle(x, y + h + 1, 1, 1));

            BitmapContent.Copy(source, new Rectangle(w - 1, h - 1, 1, 1),
                               output, new Rectangle(x + w + 1, y + h + 1, 1, 1));

            PackedBitmap pack = new PackedBitmap();
            pack.Offset = new Rectangle(x + 1, y + 1, source.Width, source.Height);
            pack.Sprite = source;
            pack.Bitmap = output;
            pack.Texture = texture;

            bucket.Add(key, pack);
        }

        protected virtual bool BuildTexture(ExternalReference<TextureContent> reference, 
            ContentProcessorContext context, out string key, out TextureContent texture)
        {
            if (reference.Filename.EndsWith(".xnb")) 
            {
                key = reference.Filename;
                texture = null;
                return false;
            }

            OpaqueDataDictionary processorParameters = new OpaqueDataDictionary();
            processorParameters.Add("ColorKeyColor", this.ColorKeyColor);
            processorParameters.Add("ColorKeyEnabled", this.ColorKeyEnabled);
            processorParameters.Add("TextureFormat", this.TextureFormat);
            processorParameters.Add("GenerateMipmaps", true);
            processorParameters.Add("ResizeToPowerOfTwo", false);

            texture = context.BuildAndLoadAsset<TextureContent, TextureContent>(
                reference, typeof(TextureProcessor).Name, processorParameters, null);

            key = reference.Filename;
            return true;
        }

        private void BuildAllFonts(UiThemeContent input, ContentProcessorContext context)
        {
            foreach (ThemeFontContent fontContent in input.Fonts)
            {
                fontContent.Font = BuildFont(fontContent.Font, context);
            }
        }

        private ExternalReference<SpriteFontContent> BuildFont(ExternalReference<SpriteFontContent> font, ContentProcessorContext context)
        {
            if (font.Filename.EndsWith(".xnb"))
            {
                return font;
            }

            if (font.Filename.EndsWith(".spritefont"))
            {
                ExternalReference<FontDescription> fd = new ExternalReference<FontDescription>(font.Filename);
                return context.BuildAsset<FontDescription, SpriteFontContent>(fd, typeof(FontDescriptionProcessor).Name, null, null, null);
            }
            else
            {
                return context.BuildAsset<SpriteFontContent, SpriteFontContent>(font, typeof(SpriteTextureProcessor).Name, null, null, null);
            }
        }
    }
}